/******************************************************************************
*  Copyright(c) 2021-2023 Sietium
*
*  @file:    fog.cpp
*  @brif:    fog
*
*  @date:    05/10/2021
*  @Author:  cary.Zhang
*
******************************************************************************/
#include <string.h>
#include <string>
#include <cstdlib>
#include <vector>
#include <array>
#include <memory>
#include "vmath.h"
#include "glad/gl-headers.h"

#include <model.h>
#include <meshgrid.h>
#include "glslprogram.h"
#include "texture.h"
#include "objectfactory.h"
#include "node.h"
#include "window.h"

class Fog : public Node
{
public:
    Fog();
protected:
    bool startup();
    void render(double currentTime, double difTime);
    void shutdown();
    bool checkRendererVersion();
private:
    GLSLProgram programClear, programAppend, programResolve;

    GLuint fragmentBuffer;
    GLuint texture;
    GLuint atomicCounterBuffer;
    GLuint vao;
    GLuint vbo;

    std::unique_ptr<Model> model;
    GLfloat indexInterval;
};

Fog::Fog()
{
    nodeName = "Fog";
    indexInterval = 1.0f;
}

bool Fog::startup()
{
#ifdef USE_GLES
    /* Disable fog on GLES for now */
    return false;
#endif
    // glBindImageTexture/glMemoryBarrier added in 4.2 or ARB_shader_image_load_store
    // glTexStorage2D added in 4.2 or ARB_texture_storage
    // GL_ATOMIC_COUNTER_BUFFER added in 4.2 or ARB_shader_atomic_counters
    // GL_SHADER_STORAGE_BUFFER added in 4.3 or ARB_shader_storage_buffer_object
    // Binding points added in 4.2 or ARB_shading_language_420pack
    if (PerfWindow::glVersion < 430 &&
        (PerfWindow::glVersion < 400 ||
         !GLAD_GL_ARB_shader_image_load_store ||
         !GLAD_GL_ARB_shader_storage_buffer_object ||
         !GLAD_GL_ARB_shader_atomic_counters ||
         !GLAD_GL_ARB_texture_storage ||
         !GLAD_GL_ARB_shading_language_420pack)) {
        printf("Failed! The GL version is less than 4.3 or 4.0 without extensions:\n"
            "GL_ARB_shader_image_load_store\n"
            "GL_ARB_shader_storage_buffer_object\n"
            "GL_ARB_shader_atomic_counters\n"
            "GL_ARB_texture_storage\n"
            "GL_ARB_shading_language_420pack\n");
        return false;
    }

    GLint fragMaxAtomic = 0;
    glGetIntegerv(GL_MAX_FRAGMENT_ATOMIC_COUNTERS, &fragMaxAtomic);
    if (fragMaxAtomic <= 0) {
        std::cout << nodeName << " failed : GL_MAX_FRAGMENT_ATOMIC_COUNTERS is 0" << std::endl;
        return false;
    }

    GLint maxShaderStorage = 0;
    glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxShaderStorage);
    if (maxShaderStorage <= 0) {
        std::cout << nodeName << " failed : GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is 0" << std::endl;
        return false;
    }

    if (PerfWindow::glVersion > 400) {
        if (!programClear.build("../media/shaders/fog/430/clear.vs.glsl", "../media/shaders/fog/430/clear.fs.glsl"))
            return false;

        if (!programAppend.build("../media/shaders/fog/430/append.vs.glsl",
                                "../media/shaders/fog/430/append.fs.glsl"))
            return false;

        if (!programResolve.build("../media/shaders/fog/430/resolve.vs.glsl",
                                "../media/shaders/fog/430/resolve.fs.glsl"))
            return false;
    }
    else {
        if (!programClear.build("../media/shaders/fog/400/clear.vs.glsl", "../media/shaders/fog/400/clear.fs.glsl"))
            return false;

        if (!programAppend.build("../media/shaders/fog/400/append.vs.glsl",
                                "../media/shaders/fog/400/append.fs.glsl"))
            return false;

        if (!programResolve.build("../media/shaders/fog/400/resolve.vs.glsl",
                                "../media/shaders/fog/400/resolve.fs.glsl"))
            return false;
    }

    model.reset(new Model);
    model->loadModel("../media/objects/bunny.obj");
    GLint screenW = getWidth();
    GLint screenH = getHeight();
    GLint ssboSize = screenW * screenH * 4 * 100;
    while (ssboSize <= 0 && getWidth() > 0 && getHeight() > 0) {
        screenW /= 2;
        screenH /= 2;
        ssboSize = screenW * screenH * 4 * 100;
    }

    glGenBuffers(1, &fragmentBuffer);
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, fragmentBuffer);
    glBufferData(GL_SHADER_STORAGE_BUFFER, ssboSize, NULL, GL_DYNAMIC_COPY);

    glGenBuffers(1, &atomicCounterBuffer);
    glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, atomicCounterBuffer);
    glBufferData(GL_ATOMIC_COUNTER_BUFFER, 4, NULL, GL_DYNAMIC_COPY);

    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, getWidth(), getHeight());
    makeScreenVao(vao,vbo);

    return true;
}

void Fog::render(double currentTime, double difTime)
{
    (void)difTime;
    static const GLfloat gray[] = {0.6f, 0.2f, 0.2f, 1.0f };
    float f = currentTime * 1.5f;

    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
                    GL_SHADER_STORAGE_BARRIER_BIT);
    glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI);

    programClear.use();
    glBindVertexArray(vao);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glClearBufferfv(GL_COLOR, 0, gray);
    programAppend.use();

    vmath::mat4 modelMatrix = vmath::scale(7.0f);
    vmath::vec3 viewPosition = vmath::vec3(cosf(f * 0.35f) * 120.0f, cosf(f * 0.4f) * 10.0f,
                                            sinf(f * 0.35f) * 120.0f);
    vmath::mat4 viewMatrix = vmath::lookat(viewPosition,
                                            vmath::vec3(0.0f, 0.0f, 0.0f),
                                            vmath::vec3(0.0f, 1.0f, 0.0f));
    vmath::mat4 zoomMatrix = vmath::scale(6.00f, 6.00f, 6.00f);
    vmath::mat4 mvMatrix = viewMatrix * modelMatrix * zoomMatrix;
    vmath::mat4 projMatrix = vmath::perspective(50.0f,
                                                 getWindowRatio(),
                                                 0.1f,
                                                 1000.0f);
    programAppend.setUniform1f("indexInterval", indexInterval);
    programAppend.setUniformMatrix4fv("mvp", (projMatrix * mvMatrix).operator float * ());

    static const unsigned int zero = 0;
    glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, atomicCounterBuffer);
    glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(zero), &zero);
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, fragmentBuffer);
    glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI);

    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
                    GL_SHADER_STORAGE_BARRIER_BIT);

    model->renderModel();
    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
                    GL_SHADER_STORAGE_BARRIER_BIT);
    glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, atomicCounterBuffer);
    GLuint *atomicCount = static_cast<GLuint *>(glMapBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0,
                                                                 4, GL_MAP_READ_BIT));
    indexInterval = static_cast<GLfloat>(*atomicCount) / 300000.0f;
    if (indexInterval < 1.0f) {
        indexInterval = 1.0f;
    }
    glUnmapBuffer(GL_ATOMIC_COUNTER_BUFFER);
    glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
    programResolve.use();

    glBindVertexArray(vao);
    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
                    GL_SHADER_STORAGE_BARRIER_BIT);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

void Fog::shutdown()
{
    programClear.clear();
    programAppend.clear();
    programResolve.clear();

    glDeleteBuffers(1, &fragmentBuffer);
    glDeleteBuffers(1, &atomicCounterBuffer);
    glDeleteTextures(1, &texture);
    glDeleteVertexArrays(1, &vao);
    glDeleteBuffers(1,&vbo);

    if (model)
        model->clearModel();
}
bool Fog::checkRendererVersion()
{
    const GLubyte *renderer = glGetString(GL_RENDERER);
    GLint versionLimit[] = {9, 0, 0};

    std::string rendererStr((char *)const_cast<GLubyte *>(renderer));

    GLint versionNumPos = -1;
    GLuint i = 0;
    bool isFindLLVM = false;
    for (; i < rendererStr.size(); i++) {
        if (rendererStr.at(i) == 'l' \
                || rendererStr.at(i) == 'v' \
                || rendererStr.at(i) == 'm' \
                || rendererStr.at(i) == 'L' \
                || rendererStr.at(i) == 'V' \
                || rendererStr.at(i) == 'M') {
            isFindLLVM = true;
        } else if (isFindLLVM && rendererStr.at(i) == ' ') {
            versionNumPos = i + 1;
            break;
        } else {
            isFindLLVM = false;
        }
    }
    if (isFindLLVM) {
        if (versionNumPos != -1) {
            GLuint j = static_cast<GLuint>(versionNumPos);
            std::string versionNumStr;
            versionNumStr.resize(rendererStr.size());
            std::vector<GLint> versionNum(rendererStr.size());
            GLuint k = 0;
            GLuint l = 0;
            for (; j < rendererStr.size(); j++) {
                if (rendererStr.at(j) >= '0' \
                        && rendererStr.at(j) <= '9') {
                    versionNumStr[k] = rendererStr.at(j);
                    k++;
                } else if (k > 0) {
                    versionNum[l] = atoi(versionNumStr.data());
                    l++;
                    versionNumStr.clear();
                    versionNumStr.resize(rendererStr.size());
                    k = 0;
                    if (rendererStr.at(j) != '.') {
                        break;
                    }
                }
            }
            if (l <= 0) {
                return false;
            }
            if (versionNum[0] >= versionLimit[0]) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
    return true;
}

REGISTER_OBJECT(Node, Fog)
